//!
//! This module contains the implementation of the blockchain P2P network.
//!
//!

//
// https://developer.bitcoin.org/devguide/p2p_network.html
// https://developer.bitcoin.org/reference/p2p_networking.html
//
// Peer Discovery
// Connecting To Peers
// Initial Block Download
//      Blocks-First
//      Blocks-First Advantages & Disadvantages
//      Headers-First
// Block Broadcasting
// Orphan Blocks
// Transaction Broadcasting

use std::io::Write;

use libp2p::kad;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncBufReadExt;
use tracing::info;

use crate::network_api::{P2PEventForApp, P2pNetWorkHandlerForApp};
use crate::network_core::network_core_inner_api::NetWorkCoreInnerApi;

pub const BC_FLOOD_TOPIC_NAME_PREFIX: &str = "mini-bc-boradcast";
pub use crate::network_core::P2pNetWorkError;
pub use crate::network_core::network_core_impl::combined_behaviour::ContentResponse;
pub use libp2p::{PeerId, core::Multiaddr, request_response::ResponseChannel};

const HASH_256_BITS_BYTES_LENGTH: usize = 32;
pub type Hash256Bytes = [u8; HASH_256_BITS_BYTES_LENGTH];

#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub struct ProvidingContentId(Hash256Bytes); // sha2::Sha256 -- 256 bits;

impl ProvidingContentId {
    pub fn new(hash: Hash256Bytes) -> Self {
        Self(hash)
    }

    pub fn as_bytes_ref(&self) -> &Hash256Bytes {
        &self.0
    }

    pub fn from_hex_str(hex_string: &str) -> Result<Self, hex::FromHexError> {
        let bytes = hex::decode(hex_string)?;
        if bytes.len() != HASH_256_BITS_BYTES_LENGTH {
            return Err(hex::FromHexError::InvalidStringLength);
        }
        let mut hash_bytes = [0; HASH_256_BITS_BYTES_LENGTH];
        hash_bytes.copy_from_slice(&bytes);
        Ok(Self(hash_bytes))
    }

    pub fn as_bytes(&self) -> Hash256Bytes {
        self.0
    }

    pub fn to_hex_string(&self) -> String {
        hex::encode(self.0)
    }

    pub fn to_record_key(&self) -> kad::RecordKey {
        kad::RecordKey::new(self.as_bytes_ref())
    }
}

impl std::fmt::Display for ProvidingContentId {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.to_hex_string())
    }
}

impl std::fmt::Debug for ProvidingContentId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.to_hex_string())
    }
}

pub mod network_api;
mod network_core;

#[derive(Debug, thiserror::Error)]
pub enum BcP2pError {
    #[error("P2P network error {0}")]
    P2pNetWorkError(#[from] crate::network_core::P2pNetWorkError),

    #[error("Multiaddr error {0}")]
    MultiaddrError(#[from] libp2p::multiaddr::Error),

    #[error("IO error {0}")]
    IoError(#[from] std::io::Error),

    #[error("Expect peer multiaddr to contain peer ID")]
    ExpectPeerMultiaddrToContainPeerId,

    #[error("Could not find provider for file {0}.")]
    CouldNotFindProviderForContent(ProvidingContentId),

    #[error("None of the providers returned file.")]
    NoneOfTheProvidersReturnedFile,

    #[error("Oneshot ReceiveError {0}")]
    OneshotRecvError(#[from] tokio::sync::oneshot::error::RecvError),

    #[error("Network handler for APP is None")]
    NetworkHandlerForAppIsNone,

    #[error("p2p event receiver for app is None")]
    P2pEventReceiverForAppIsNone,
}

#[derive(Debug)]
pub struct BcP2PNetwork {
    network_inner_api: NetWorkCoreInnerApi,
    network_handler: Option<P2pNetWorkHandlerForApp>,
    network_task: Option<tokio::task::JoinHandle<()>>,
}

impl BcP2PNetwork {
    pub async fn new_and_start(
        secret_key_seed: Option<u8>,
        listen_address: Option<Multiaddr>,
        shutdown_signal: tokio::sync::watch::Receiver<bool>,
    ) -> Result<(Self, tokio::sync::mpsc::Receiver<P2PEventForApp>), BcP2pError> {
        let (network_inner_api, command_sender_for_app, event_receiver_for_app, p2p_manager) =
            network_core::network_core_impl::new_network_core(secret_key_seed, listen_address)
                .await?;

        let network_task =
            tokio::task::spawn(p2p_manager.network_core_main_loop(shutdown_signal.clone()));

        let network_handler = P2pNetWorkHandlerForApp::new(command_sender_for_app);

        let network = Self {
            network_inner_api,
            network_handler: Some(network_handler),
            network_task: Some(network_task),
        };

        Ok((network, event_receiver_for_app))
    }

    pub fn take_network_handler_for_app(&mut self) -> Result<P2pNetWorkHandlerForApp, BcP2pError> {
        match self.network_handler.take() {
            Some(network_handler) => Ok(network_handler),
            None => Err(BcP2pError::NetworkHandlerForAppIsNone),
        }
    }

    pub async fn shutdown(&mut self) {
        // self.network_inner_api.cleanup_swarm().await.unwrap();

        if let Some(handle) = self.network_task.take() {
            handle.await.unwrap();
        }
    }

    pub async fn cleanup(&mut self) {
        self.network_inner_api.cleanup_swarm().await.unwrap();
    }
}

pub fn crate_input_listener_for_p2p() -> tokio::task::JoinHandle<()> {
    tokio::task::spawn(async move {
        let mut stdin = tokio::io::BufReader::new(tokio::io::stdin()).lines();
        print!("> ");
        std::io::stdout().flush().unwrap();
        while let Ok(Some(line)) = stdin.next_line().await {
            let line = line.trim();
            if !line.is_empty() {
                info!("Received input: {}", line);
            }
            let helper = || {
                println!("Available commands: exit, help");
            };

            if line.eq_ignore_ascii_case("exit") {
                break;
            } else if line.eq_ignore_ascii_case("help") {
                helper();
            } else if !line.is_empty() {
                println!("Unknown command: {}", line);
                helper();
            }

            print!("> ");
            std::io::stdout().flush().unwrap();
        }
    })
}
